#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
playlistbs - Display episodes the member has not seen yet (according to
www.betaseries.com), find them on the computer, mark them as downloaded,
download the subtitles and create a playlist of these episodes.

@author: Romain RICHARD
@contact: romain.richard.IT.engineer@gmail.com
@version: 2.0
@copyright: GPLv3

NEW OPTIONS
@todo: Add a daemon mode that will update itself every _update_frequency second
@todo: Search if there is an update of the script
@todo: Create a GUI http://stackoverflow.com/questions/115495/
       is-python-any-good-for-gui-development/115654#115654

@change: 2012-09-17: Used pylint to clean up the code
@change: 2011-11-20: Deleted the config file option
@change: 2011-08-06: Refactoring of many methods, added the --auto-add-shows option
@change: 2011-08-01: Created the playlistbs package
@change: 2011-08-01: Created __init__.py, __main__.py, api.py, episode.py
                     and show.py
@change: 2011-07-17: Added the --limit option (replace the --first option)
@change: 2011-07-16: Added the --pilots option
@change: 2011-07-15: Creation of playlist ok
@change: 2011-07-14: Reorganisation of functions,
                     creation of the playlist started
@change: 2011-07-13: Creation of the Show and Episode classes, _episodes_to_xml
                     and _episodes_to_txt functions
@change: 2011-07-12: Docstrings, public/private functions, PEP8 Checker
@change: 2011-07-10: Creation of self._not_seen_xml_file,
                     self._not_seen_txt_file and self._shows_file ok
"""

import hashlib

from operator import attrgetter
from os import makedirs, walk
from os.path import dirname, exists, expanduser, join, realpath, split
from re import match
from time import time
from xml.dom.minidom import Document, parse
from xml.parsers.expat import ExpatError

from playlist_betaseries.api import API
from playlist_betaseries.episode import Episode
from playlist_betaseries.show import Show
from playlist_betaseries.subtitle import Subtitle


class PlaylistBS(object):
    """Display episodes the member has not seen yet (according to
    www.betaseries.com), find them on the computer, mark them as downloaded,
    download the subtitles and create a playlist of these episodes.
    """

    def __init__(self, base_config):
        """Constructor.

        @type base_config: String
        @param base_config: The folder where the files needed by playlistbs
                            will be created

        """
        self._api = None
        self._key = "f3df045397dc"
        self._login = None
        self._password = None

        self._shows = []
        self._episodes = []
        self._episodes_downloaded = None
        self._auto_added_shows = []

        self._not_seen_xml_file = expanduser(base_config + "/notseen.xml")
        self._not_seen_txt_file = expanduser(base_config + "/notseen.txt")
        self._shows_file = expanduser(base_config + "/shows.xml")
        self._playlist_file = expanduser(base_config + "/playlist.pls")

        base = dirname(self._shows_file)
        if not exists(base):
            try:
                makedirs(base)
            except OSError, e:
                print(e)

    # TODO: Use ConfigParser
    def set_config(self, conf):
        """Set the configuration.

        @type conf: Dictionary
        @param conf: The configuration

        """
        for k, v in conf.items():
            if v == None:
                continue
            elif k == "login":
                self._login = v
            elif k == "password":
                self._password = hashlib.new("md5", v).hexdigest()
            elif k == "password_md5":
                self._password = v
            elif k == "update_frequency":
                self._update_frequency = int(v)
            elif k == "limit":
                self._limit = int(v)
            elif k == "sort_by_date":
                self._sort_by_date = (str(v) == "True")
            elif k == "folder":
                self._folder = unicode(v, "utf-8")
            elif k == "subtitles_to_download":
                self._subtitles_to_download = int(v)
            elif k == "create_playlist":
                self._create_playlist = (str(v) == "True")
            elif k == "mark_episodes_downloaded":
                self._mark_episodes_downloaded = (str(v) == "True")
            elif k == "pilots":
                self._pilots = (str(v) == "True")
            elif k == "auto_add_shows":
                self._auto_add_shows = (str(v) == "True")

    def get_login(self):
        """Return the login."""
        return self._login

    def get_password(self):
        """Return the password."""
        return self._password

    def get_download_subtitles(self):
        """Return True if the subtitles need to be downloaded."""
        return self._subtitles_to_download != 0

    def get_create_playlist(self):
        """Return True if the playlist needs to be created."""
        return self._create_playlist

    def get_mark_episodes_downloaded(self):
        """Return True if the episodes need to be mark as downloaded."""
        return self._mark_episodes_downloaded

    def is_uptodate(self):
        """Return True if the files are up to date."""
        limit = self._limit
        if exists(self._not_seen_xml_file):
            doc = parse(self._not_seen_xml_file)
            limit = int(doc.getElementsByTagName("limit")[0].firstChild.data)
        return (time() - self._get_last_update() < self._get_update_frequency() and
                (0 <= self._limit <= limit or limit == -1))

    def login(self):
        """Login to Betaseries."""
        print("[~] Logging in...")
        self._api = API(self._key)
        xmldoc = self._api.send_request(self._api.members_auth_url,
                    {"login": self._login, "password": self._password})
        token = xmldoc.getElementsByTagName("token")[0].firstChild.data
        self._api.set_token(token)

    def update_shows_list(self):
        """Update the shows list."""
        print("[~] Updating shows list...")
        members_infos = self._download_members_infos()
        shows = self._update_shows(members_infos)
        self._save_shows(shows)

    def update_episodes_list(self):
        """Update the episodes list."""
        print("[~] Updating episodes list...")
        episodes = self._download_members_episodes_all()
        episodes_sorted = self._sort_episodes(episodes)
        self._save_episodes(episodes_sorted)

    def mark_episodes_downloaded(self):
        """Mark the episodes downloaded."""
        if self._episodes_downloaded is None:
            self._episodes_downloaded = self._get_episodes_downloaded()

        print("[~] Marking episodes downloaded...")
        episodes_not_marked_downloaded = [ep for ep in self._episodes if not ep.downloaded]
        for episode in set(self._episodes_downloaded["episodes"]).intersection(set(episodes_not_marked_downloaded)):
            self._api.send_request(
                "{downloaded}{show}.xml".format(downloaded=self._api.members_downloaded_url, show=episode.show.url),
                {"season": episode.season, "episode": episode.episode}
                                   )

    def download_subtitles(self):
        """Download the subtitles."""
        if self._episodes_downloaded is None:
            self._episodes_downloaded = self._get_episodes_downloaded()

        print("[~] Downloading subtitles...")
        for episode in self._episodes_downloaded["episodes"] + self._episodes_downloaded["pilots"]:
            if episode.show.vf_subs or episode.show.vo_subs:
                self._download_subtitle(episode)

    def create_playlist(self):
        """Create the playlist."""
        if self._episodes_downloaded is None:
            self._episodes_downloaded = self._get_episodes_downloaded()

        print("[~] Creating playlist...")
        episode_files = []
        for i in range(len(self._episodes)):
            ep = self._episodes[i]
            if (ep in self._episodes_downloaded["episodes"] and
                (i == 0 or
                 self._episodes[i - 1].show.title != ep.show.title or
                 self._episodes[i - 1].show.title == ep.show.title and self._episodes[i - 1] in episode_files)
                or i > 0 and self._episodes[i - 1].show.title == ep.show.title and self._episodes[i - 1].double):
                episode_files.append(ep)

        self._episodes_downloaded["pilots"].sort(key=attrgetter('show.title', 'season', 'episode'))
        episode_files += self._episodes_downloaded["pilots"]

        playlist = []
        for i in range(0, len(episode_files)):
            playlist.append(self._create_playlist_item(episode_files[i], i))

        self._save_playlist(playlist)

    def display_episodes(self):
        """Display episodes not seen yet."""
        try:
            f = open(self._not_seen_txt_file, "r")
        except IOError:
            return

        shows_displayed = {}
        for line in f:
            show = ""
            r = match("(?P<show>.*)\sS\d{2}E\d{2}", line)
            if r:
                show = r.group("show")
            if show:
                if show not in shows_displayed:
                    shows_displayed[show] = 1
                else:
                    shows_displayed[show] += 1
                if shows_displayed[show] <= self._limit or self._limit == -1:
                    print(line.strip())

        f.close()

    def _get_update_frequency(self):
        """Return the update frequency."""
        return self._update_frequency

    def _get_last_update(self):
        """Return the time of the last update."""
        last_update = 0
        if exists(self._not_seen_xml_file):
            doc = parse(self._not_seen_xml_file)
            last_update = int(doc.getElementsByTagName("date")[0].firstChild.data)
        return last_update

    def _download_members_infos(self):
        """Download the infos of the member.

        @rtype: Document
        @return: A Document containing the member's infos

        """
        return self._api.send_request(self._api.members_infos_url)

    def _update_shows(self, members_infos):
        """Update the shows.

        @type members_infos: Document
        @param members_infos: A Document containing the member's infos

        @rtype: List
        @return: The list of shows not archived of the member

        """
        shows = []
        shows_from_update = []
        for show in members_infos.getElementsByTagName("show"):
            if not int(show.getElementsByTagName("archive")[0].firstChild.data.strip()):
                shows_from_update.append(
                        (show.getElementsByTagName("title")[0].firstChild.data.strip(),
                         show.getElementsByTagName("url")[0].firstChild.data.strip())
                                         )

        shows_from_file = []
        if exists(self._shows_file):
            shows_file = []
            f = open(self._shows_file, 'r')
            try:
                shows_file = parse(f).getElementsByTagName("show")
            except ExpatError:
                pass
            f.close()
            shows_from_file = [
                    (show.getElementsByTagName("title")[0].firstChild.data.strip(),
                     show.getElementsByTagName("url")[0].firstChild.data.strip())
                               for show in shows_file]

            for show in shows_file:
                title = show.getElementsByTagName("title")[0].firstChild.data.strip()
                url = show.getElementsByTagName("url")[0].firstChild.data.strip()
                if (title, url) in shows_from_update:
                    vf_subs = int(show.getElementsByTagName("VF")[0].firstChild.data.strip())
                    vo_subs = int(show.getElementsByTagName("VO")[0].firstChild.data.strip())
                    shows.append(Show(title=title, url=url, vf_subs=vf_subs, vo_subs=vo_subs))

        new_shows = list(set(shows_from_update) - set(shows_from_file))
        new_shows.sort()
        for (new_show, url) in new_shows:
            shows.append(Show(title=new_show, url=url))
            print("    [+] Added {new_show}".format(new_show=new_show))

        return shows

    def _save_shows(self, shows):
        """Save the shows list.

        @type shows: List
        @param shows: The list of shows of the member

        """
        self._shows = shows

        f = open(self._shows_file, "w")
        f.write(self._shows_to_xml(shows))
        f.close()

    def _shows_to_xml(self, shows):
        """Convert the shows list to an XML Document.

        @type shows: List
        @param shows: The list of shows of the member

        @rtype: Document
        @return: A Document containing the shows list of the member

        """
        shows_xml = Document()
        root = shows_xml.createElement("root")
        shows_xml.appendChild(root)

        for show in shows:
            root.appendChild(show.create_node(shows_xml))

        return shows_xml.toprettyxml().encode("utf-8")

    def _download_members_episodes_all(self):
        """Download the episodes list the member has not seen yet.

        @rtype: Document
        @return: A Document containing the episodes list
                 the member has not seen yet

        """
        return self._api.send_request(
                    self._api.members_episodes_all_url,
                    ({"view": str(self._limit)} if (self._limit >= 0) else {}))

    def _sort_episodes(self, episodes):
        """Sort the episodes list the member has not seen yet.

        @type episodes: Document
        @param episodes: A Document containing the episodes list
                         the member has not seen yet

        @rtype: List
        @return: The sorted list of episodes the member has not seen yet

        """
        episodes_sorted = []

        shows = Document()
        try:
            shows = parse(open(self._shows_file, 'r'))
        except (ExpatError, IOError):
            pass

        if self._sort_by_date:
            shows_added = {}
            for episode in episodes.getElementsByTagName("episode"):
                if episode.getElementsByTagName("show"):
                    show = episode.getElementsByTagName("show")[0].firstChild.data.strip()
                    if self._limit == -1 or show not in shows_added or shows_added[show] < self._limit:
                        ep = Episode().from_node(episode)
                        ep.show = self._get_show(show)
                        episodes_sorted.append(ep)
                        if show in shows_added:
                            shows_added[show] += 1
                        else:
                            shows_added[show] = 1
            episodes_sorted.sort(key=attrgetter("date"))
        else:
            for show in shows.getElementsByTagName("show"):
                show_title = show.getElementsByTagName("title")[0].firstChild.data.strip()
                nb_ep_added = 0

                for episode in episodes.getElementsByTagName("episode"):
                    if (episode.getElementsByTagName("show") and
                        (self._limit == -1 or nb_ep_added < self._limit) and
                        episode.getElementsByTagName("show")[0].firstChild.data.strip() == show_title):
                        ep = Episode().from_node(episode)
                        ep.show = self._get_show(show_title)
                        episodes_sorted.append(ep)
                        nb_ep_added += 1

        return episodes_sorted

    def _get_show(self, title):
        """Return the show corresponding to the title.

        @type title: String
        @param title: The title of the show

        @rtype: Show
        @return: The show corresponding to the title

        """
        show = Show()
        for show_ in self._shows:
            if self._strip(title) == self._strip(show_.title):
                show = show_
        if not show:
            shows = self._api.send_request(
                                           self._api.shows_search_url,
                                           {"title": self._strip(title, " ")}
                                           ).getElementsByTagName("show")
            if len(shows) == 1:
                show.title = shows[0].getElementsByTagName("title")[0].firstChild.data.strip()
                show.url = shows[0].getElementsByTagName("url")[0].firstChild.data.strip()
                if self._auto_add_shows and show not in self._auto_added_shows:
                    self._api.send_request("{url}{show}.xml".format(url=self._api.shows_add_url, show=show.url))
                    print("    [+] Added {show} to your list on betaseries".format(show=show.title))
                    self._auto_added_shows.append(show)
        if not show:
            show = Show(title=title)
        return show

    def _strip(self, title, sub=""):
        """Strip down the title of the show for better matching.

        @type title: String
        @param title: The title of the show

        @type sub: String
        @param sub: The string that will replace the characters matched

        @rtype: String
        @return: The title stripped down

        """
        title = title.split("(")[0]
        chars = [" ", "_", "-", ":", ",", "."]
        for c in chars:
            title = title.replace(c, sub)
        return title.lower()

    def _save_episodes(self, episodes):
        """Save the episodes list the member has not seen yet.

        @type episodes: List
        @param episodes: The sorted list of episodes the member has not seen yet

        """
        self._episodes = episodes

        f = open(self._not_seen_txt_file, "w")
        f.write(self._episodes_to_txt(episodes))
        f.close()

        f = open(self._not_seen_xml_file, "w")
        f.write(self._episodes_to_xml(episodes))
        f.close()

    def _episodes_to_txt(self, episodes):
        """Convert the episodes list to a String.

        @type episodes: List
        @param episodes: The sorted list of episodes the member has not seen yet

        @rtype: String
        @return: A String containing the episodes list the member has not seen yet

        """
        return "\n".join([str(episode) for episode in episodes])

    def _episodes_to_xml(self, episodes):
        """Convert the episodes list to an XML Document.

        @type episodes: List
        @param episodes: The sorted list of episodes the member has not seen yet

        @rtype: Document
        @return: A Document containing the episodes list the member has not seen yet

        """
        episodes_xml = Document()
        root = episodes_xml.createElement("root")
        episodes_xml.appendChild(root)

        date = episodes_xml.createElement("date")
        root.appendChild(date)
        date_value = episodes_xml.createTextNode(str(int(time())))
        date.appendChild(date_value)

        limit = episodes_xml.createElement("limit")
        root.appendChild(limit)
        limit_value = episodes_xml.createTextNode(str(int(self._limit)))
        limit.appendChild(limit_value)

        last_show = ""
        for episode in episodes:
            if last_show != episode.show.title:
                show = episodes_xml.createElement("show")
                root.appendChild(show)

                title = episodes_xml.createElement("title")
                show.appendChild(title)
                title_value = episodes_xml.createTextNode(episode.show.title)
                title.appendChild(title_value)

                last_show = episode.show.title

            show.appendChild(episode.create_node(episodes_xml))

        return episodes_xml.toprettyxml().encode("utf-8")

    def _get_episodes_downloaded(self):
        """Find the episode files in the folder.

        @rtype: List
        @return: A List containing the Episodes

        """
        print("[~] Scanning the computer...")
        self._folder = realpath(self._folder)
        episode_files = []
        episode_files_unsorted = []
        pilot_files = []
        episodes = self._episodes[:]

        for root, _, files in walk(self._folder):
            for file_ in files:
                ep = Episode().from_file(root, file_)
                if ep:
                    # TODO: Keep ep.show.title in a list (or file?) to reduce
                    # the number of requests
                    ep.show = self._get_show(ep.show.title)
                    # TODO: Fix this, it's bad
                    if ep in episodes:
                        i = episodes.index(ep)
                        episodes[i] += ep
                        episode_files_unsorted.append(episodes[i])
                        del episodes[i]
                    elif ep.is_pilot() and ep.show not in self._shows:
                        pilot_files.append(ep)

        for ep in self._episodes:
            if ep in episode_files_unsorted:
                episode_files.append(ep)

        return {"episodes": episode_files, "pilots": pilot_files}

    def _download_subtitle(self, episode):
        """Download a subtitle for an episode.

        @type episode: Episode
        @param episode: The episode

        """
        existing_subtitles = self._find_existing_subtitles(episode)
        best_existing_subtitles = self._find_best_subtitles(existing_subtitles)

        available_subtitles = self._download_available_subtitles(episode)
        best_available_subtitles = self._limit_subtitles_to_download(self._find_best_subtitles(available_subtitles))

        for sub in best_available_subtitles:
            better_than = sub.is_better_than(best_existing_subtitles)
            if better_than or not best_existing_subtitles:
                sub.download()
                for s in better_than:
                    s.delete()

    def _find_existing_subtitles(self, episode):
        """Find existing subtitles for an episode.

        @type episode: Episode
        @param episode: The episode

        @rtype: List of Subtitle
        @return: List of the available subtitles for the episode

        """
        existing_subtitles = []
        folder = split(episode.file)[0]
        for root, _, files in walk(folder):
            for file_ in files:
                sub = Subtitle().from_file(episode, join(root, file_))
                if sub:
                    existing_subtitles.append(sub)
        return existing_subtitles

    def _download_available_subtitles(self, episode):
        """Download the available subtitles for an episode.

        @type episode: Episode
        @param episode: The episode

        @rtype: List of Subtitle
        @return: List of the available subtitles for the episode

        """
        subs = []
        if episode.show.vf_subs or episode.show.vo_subs:
            variables = {"file": split(episode.file)[1]}
            if episode.show.vf_subs and not episode.show.vo_subs:
                variables["language"] = "VF"
            elif not episode.show.vf_subs and episode.show.vo_subs:
                variables["language"] = "VO"
            subs = self._api.send_request(self._api.subtitles_show_url, variables).getElementsByTagName("subtitle")
        return [Subtitle().from_node(episode, sub) for sub in subs]

    def _find_best_subtitles(self, subtitles):
        """Find the best subtitles for each language in the subtitles list for an episode.

        @type subtitles: List
        @param subtitles: List of the available subtitles for the episode

        @rtype: List
        @return: The best subtitles for each language in the subs list for the episode

        """
        best_subtitles = subtitles[:]
        for subtitle in subtitles:
            better_than = subtitle.is_better_than(subtitles)
            for sub in better_than:
                if sub in best_subtitles:
                    best_subtitles.remove(sub)
        return best_subtitles

    def _limit_subtitles_to_download(self, subtitles):
        """Limit the number of subtitles to download in each language for an
        episode.

        @type subtitles: List of Subtitle
        @param subtitles: The available subtitles to download

        @rtype: List of Subtitle
        @return: The list of subtitles to download

        """
        subtitles_to_download = []
        nb_subtitles_to_download = (self._subtitles_to_download if self._subtitles_to_download > 0 else len(subtitles))
        LANGUAGES = set([s.language for s in subtitles])
        for language in LANGUAGES:
            subtitles_to_download += [s for s in subtitles if s.language == language][:nb_subtitles_to_download]
        return subtitles_to_download

    def _create_playlist_item(self, episode, position):
        """Create a playlist item.

        @type episode: Episode
        @param episode: An episode

        @type position: Integer
        @param position: The position of the episode in the playlist

        @rtype: String
        @return: A String representing the playlist item

        """
        ep_file = "File{position}={file}".format(position=2 * position,
                                                 file=episode.file)
        ep_title = ("Title{position}={show} {number} {title}"
                    .format(position=2 * position,
                            show=episode.show.title.encode("utf-8"),
                            title=episode.title.encode("utf-8"),
                            number=episode.number))
        url_show = "{watched}{show}.xml".format(watched=self._api.members_watched_url,
                                                show=episode.show.url)
        url = self._api.create_request(url_show,
                                       {"season": episode.season,
                                        "episode": episode.episode}
                                       )
        watched_file = "File{position}={url}".format(position=2 * position + 1,
                                                     url=url)
        watched_title = ("Title{position}=[Watched] {show} {number} {title}"
                         .format(position=2 * position + 1,
                                 show=episode.show.title.encode("utf-8"),
                                 title=episode.title.encode("utf-8"),
                                 number=episode.number))
        return "\n".join([ep_file, ep_title, watched_file, watched_title])

    def _save_playlist(self, playlist):
        """Save the playlist.

        @type playlist: List
        @param playlist: A List of playlist items

        """
        with open(self._playlist_file, "w") as file_:
            file_.write("\n".join(playlist))
